package com.example.map_api_for_irs.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 2022/7/22
 *
 * @author fengyu
 */
@Controller
@RequestMapping("/yh")
public class MapController {

    @Value("${map.api.url}")
    String mapUrl;

    @Value("${map.api.token}")
    String mapToken;

    @Value("${map.api.appKey}")
    String appKey;

    @Value("${map.api.secretKey}")
    String secretKey;

    private static final String AURORA_TOKEN = "X-AURORA-TOKEN";
    private static final String APPKEY_HEADER = "X-AURORA-APPKEY";
    private static final String SIGN_HEADER = "X-AURORA-SIGN";
    private static final String TIMESTAMP_HEADER = "X-AURORA-TIMESTAMP";
    private static final String CONTENT_MD5 = "CONTENT-MD5";

    /**
     * 与页面端加密方式相同
     */
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
    @Value("${aes.iv}")
    String AES_IV = "1234123412341234";
    @Value("${aes.key}")
    String AES_KEY = "1234123412341234";

    private static final Logger log = LoggerFactory.getLogger(MapController.class);

    @GetMapping(value = "/", produces = {"image/png", "image/jpg", "image/jpeg", "text/xml", "text/html", "application/octet-stream"})
    public ResponseEntity<byte[]> getTile(HttpServletRequest request) {
        if (null != mapToken && !mapToken.isEmpty()){
            try {
                return doGet(httpGet(request.getQueryString(), mapUrl, mapToken));
            } catch (URISyntaxException e) {
                e.printStackTrace();
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .contentType(MediaType.TEXT_HTML)
                        .body("地图服务地址可能出错".getBytes(StandardCharsets.UTF_8));
            }
        } else {
            HttpGet httpGet = httpGet(request, mapUrl, appKey, secretKey);
            return doGet(httpGet);
        }
    }

    /**
     * 指定了地图服务的url和ak，sk，放到请求的token参数里，用AES加密解密
     * @param token 地图服务地址
     * @param request
     * @return
     */
    @RequestMapping(value = "/proxy", produces = {"image/png", "image/jpg", "image/jpeg", "text/xml", "text/html", "application/octet-stream"})
    public ResponseEntity<byte[]> getTile(@RequestParam String token, HttpServletRequest request) {
        String url, ak, sk, mapToken;
        try {
            // 前端加密后进行了Base64转换，所以拿到token后先进行反转换
            byte[] tokenByte = Base64.getDecoder().decode(token);

            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(AES_KEY.getBytes(), "AES"), new IvParameterSpec(AES_IV.getBytes()));
            byte[] bs = cipher.doFinal(tokenByte);

            // 解密后再进行Base64反转换，获取json格式的url ak  sk三个值
            String str = new String(Base64.getDecoder().decode(bs));
            str = URLDecoder.decode(str, StandardCharsets.UTF_8.name());
            Map<String, String> map = new ObjectMapper().readValue(str, Map.class);
            url = map.get("url");
            if (map.containsKey("token")){
                mapToken = map.get("token");
                return doGet(httpGet(request.getQueryString().replaceAll("token=[^&]*&",""), url, mapToken));
            } else {
                ak = map.get("appKey");
                sk = map.get("secretKey");
            }
        } catch (GeneralSecurityException | IOException | URISyntaxException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .contentType(MediaType.TEXT_HTML)
                    .body("地图服务地址可能出错".getBytes(StandardCharsets.UTF_8));
        }

        HttpGet httpGet = httpGet(request, url, ak, sk);
        return doGet(httpGet);
    }

    @PostConstruct
    public void initInfo(){
        StringBuilder sb = new StringBuilder();
        sb.append("\n地图代理服务已启动\n")
                .append("地图服务地址：").append(mapUrl).append("\n")
                .append("API AppKey: ").append(appKey).append("    ")
                .append("API SecretKey: ").append(secretKey).append("\n")
                .append("API token:").append(mapToken).append("\n");
        log.info(sb.toString());
    }

    private SSLContext getAllTrustedSslContext() throws GeneralSecurityException {
        TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }
        }};

        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, trustAllCerts, new SecureRandom());
        return context;
    }
    private HostnameVerifier getAllTrustedVerifier() {
        return (hostname, session) -> true;
    }

    private HttpGet httpGet(final HttpServletRequest request, final String mapUrl, final String appKey, final String secretKey) {
        StringBuilder stringBuilder = new StringBuilder();
        List<NameValuePair> nvps = request.getParameterMap().entrySet().stream()
                .filter(entry -> !"token".equalsIgnoreCase(entry.getKey()))
                .map(entry -> new BasicNameValuePair(entry.getKey().toUpperCase(), entry.getValue()[0]))
                .collect(Collectors.toList());
        nvps.sort(Comparator.comparing(NameValuePair::getName));
        URI uri;
        try {
            uri = new URIBuilder(mapUrl).addParameters(nvps).build();
        } catch (URISyntaxException e) {
            e.printStackTrace();
            return null;
        }
        HttpGet httpGet = new HttpGet(uri);
        for (NameValuePair entry : nvps){
            stringBuilder.append(entry.getName());
            if (!ObjectUtils.isEmpty(entry.getValue())){
                stringBuilder.append("=").append(entry.getValue()).append("&");
            }
        };
        if ((stringBuilder.length() - 1) == stringBuilder.lastIndexOf("&")){
            stringBuilder.deleteCharAt(stringBuilder.length() - 1);
        }
        long timestamp = System.currentTimeMillis();
        String stringToSign = "GET\n" + stringBuilder + "\n" + timestamp;
        log.info(stringToSign);
        String signature = Base64.getEncoder().encodeToString(new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey.getBytes()).hmacHex(stringToSign).getBytes());
        httpGet.setHeader(APPKEY_HEADER, appKey);
        httpGet.setHeader(SIGN_HEADER, signature);
        httpGet.setHeader(TIMESTAMP_HEADER, String.valueOf(timestamp));
        httpGet.setHeader("Accept", MediaType.ALL_VALUE);
        try {
            log.info("{} and sign : {} , timestamp : {}", uri.toURL(), signature, timestamp);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return httpGet;
    }

    private HttpGet httpGet(final String queryString, final String mapUrl, final String mapToken) throws URISyntaxException {
        URI uri= new URIBuilder(mapUrl).setCustomQuery(queryString).build();
        HttpGet httpGet = new HttpGet(uri);
        httpGet.setHeader(AURORA_TOKEN, mapToken);
        return httpGet;
    }

    private ResponseEntity<byte[]> doGet(final HttpGet httpGet){
        if (null != httpGet){
            try (CloseableHttpResponse response =  HttpClientBuilder.create()
                            .setSSLContext(getAllTrustedSslContext())
                            .setSSLHostnameVerifier(getAllTrustedVerifier()).build()
                            .execute(httpGet)) {
                HttpEntity entity = response.getEntity();
                InputStream inputStream = entity.getContent();
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                int ln;
                while ((ln = inputStream.read()) != -1){
                    byteArrayOutputStream.write(ln);
                }
                String contentType = entity.getContentType().getValue().split(";")[0];
                MediaType mediaType ;
                if (contentType.indexOf("/") > 0) {
                    String[] ss = contentType.split("/");
                    mediaType = new MediaType(ss[0], ss[1]);
                } else {
                    mediaType = new MediaType(contentType);
                }
                return ResponseEntity.status(response.getStatusLine().getStatusCode())
                        .header("Access-Control-Allow-Origin","*")
                        .contentType(mediaType).contentLength(entity.getContentLength())
                        .body(byteArrayOutputStream.toByteArray());
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (GeneralSecurityException e) {
                e.printStackTrace();
            }
            log.info(new StringBuilder("获取地图服务出错，相关参数如下：\n")
                    .append(httpGet.getURI().getQuery()).append("\n")
                    .append(SIGN_HEADER).append(" : ").append(httpGet.getFirstHeader(SIGN_HEADER).getValue()).toString());
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .contentType(MediaType.TEXT_HTML)
                .body("调用服务失败".getBytes(StandardCharsets.UTF_8));
    }
}
